home *** CD-ROM | disk | FTP | other *** search
/ Celestin Apprentice 5 / Apprentice-Release5.iso / Source Code / C / Applications / Python 1.3.3 / Python 133 68K / Lib / urllib.py < prev    next >
Text File  |  1996-05-20  |  21KB  |  735 lines

  1. # Open an arbitrary URL
  2. #
  3. # See the following document for a tentative description of URLs:
  4. #     Uniform Resource Locators              Tim Berners-Lee
  5. #     INTERNET DRAFT                                    CERN
  6. #     IETF URL Working Group                    14 July 1993
  7. #     draft-ietf-uri-url-01.txt
  8. #
  9. # The object returned by URLopener().open(file) will differ per
  10. # protocol.  All you know is that is has methods read(), readline(),
  11. # readlines(), fileno(), close() and info().  The read*(), fileno()
  12. # and close() methods work like those of open files. 
  13. # The info() method returns an mimetools.Message object which can be
  14. # used to query various info about the object, if available.
  15. # (mimetools.Message objects are queried with the getheader() method.)
  16.  
  17. import string
  18. import socket
  19. import regex
  20. import os
  21.  
  22.  
  23. __version__ = '1.2' # XXXX Should I update this number? -- jack
  24.  
  25. # Helper for non-unix systems
  26. if os.name == 'mac':
  27.     def url2pathname(pathname):
  28.         "Convert /-delimited pathname to mac pathname"
  29.         #
  30.         # XXXX The .. handling should be fixed...
  31.         #
  32.         tp = splittype(pathname)[0]
  33.         if tp and tp <> 'file':
  34.             raise RuntimeError, 'Cannot convert non-local URL to pathname'
  35.         components = string.split(pathname, '/')
  36.         # Remove . and embedded ..
  37.         i = 0
  38.         while i < len(components):
  39.             if components[i] == '.':
  40.                 del components[i]
  41.             elif components[i] == '..' and i > 0 and \
  42.                           components[i-1] not in ('', '..'):
  43.                 del components[i-1:i+1]
  44.                 i = i-1
  45.             elif components[i] == '' and i > 0 and components[i-1] <> '':
  46.                 del components[i]
  47.             else:
  48.                 i = i+1
  49.         if not components[0]:
  50.             # Absolute unix path, don't start with colon
  51.             return string.join(components[1:], ':')
  52.         else:
  53.             # relative unix path, start with colon. First replace
  54.             # leading .. by empty strings (giving ::file)
  55.             i = 0
  56.             while i < len(components) and components[i] == '..':
  57.                 components[i] = ''
  58.                 i = i + 1
  59.             return ':' + string.join(components, ':')
  60.             
  61.     def pathname2url(pathname):
  62.         "convert mac pathname to /-delimited pathname"
  63.         if '/' in pathname:
  64.             raise RuntimeError, "Cannot convert pathname containing slashes"
  65.         components = string.split(pathname, ':')
  66.         # Replace empty string ('::') by .. (will result in '/../' later)
  67.         for i in range(1, len(components)):
  68.             if components[i] == '':
  69.                 components[i] = '..'
  70.         # Truncate names longer than 31 bytes
  71.         components = map(lambda x: x[:31], components)
  72.         
  73.         if os.path.isabs(pathname):
  74.             return '/' + string.join(components, '/')
  75.         else:
  76.             return string.join(components, '/')
  77. else:
  78.     def url2pathname(pathname):
  79.         return pathname
  80.     def pathname2url(pathname):
  81.         return pathname
  82.  
  83. # This really consists of two pieces:
  84. # (1) a class which handles opening of all sorts of URLs
  85. #     (plus assorted utilities etc.)
  86. # (2) a set of functions for parsing URLs
  87. # XXX Should these be separated out into different modules?
  88.  
  89.  
  90. # Shortcut for basic usage
  91. _urlopener = None
  92. def urlopen(url):
  93.     global _urlopener
  94.     if not _urlopener:
  95.         _urlopener = FancyURLopener()
  96.     return _urlopener.open(url)
  97. def urlretrieve(url):
  98.     global _urlopener
  99.     if not _urlopener:
  100.         _urlopener = FancyURLopener()
  101.     return _urlopener.retrieve(url)
  102. def urlcleanup():
  103.     if _urlopener:
  104.         _urlopener.cleanup()
  105.  
  106.  
  107. # Class to open URLs.
  108. # This is a class rather than just a subroutine because we may need
  109. # more than one set of global protocol-specific options.
  110. # Note -- this is a base class for those who don't want the
  111. # automatic handling of errors type 302 (relocated) and 401
  112. # (authorization needed).
  113. ftpcache = {}
  114. class URLopener:
  115.  
  116.     # Constructor
  117.     def __init__(self):
  118.         server_version = "Python-urllib/%s" % __version__
  119.         self.addheaders = [('User-agent', server_version)]
  120.         self.tempcache = None
  121.         # Undocumented feature: if you assign {} to tempcache,
  122.         # it is used to cache files retrieved with
  123.         # self.retrieve().  This is not enabled by default
  124.         # since it does not work for changing documents (and I
  125.         # haven't got the logic to check expiration headers
  126.         # yet).
  127.         self.ftpcache = ftpcache
  128.         # Undocumented feature: you can use a different
  129.         # ftp cache by assigning to the .ftpcache member;
  130.         # in case you want logically independent URL openers
  131.  
  132.     def __del__(self):
  133.         self.close()
  134.  
  135.     def close(self):
  136.         self.cleanup()
  137.  
  138.     def cleanup(self):
  139.         import os
  140.         if self.tempcache:
  141.             for url in self.tempcache.keys():
  142.                 try:
  143.                     os.unlink(self.tempcache[url][0])
  144.                 except os.error:
  145.                     pass
  146.                 del self.tempcache[url]
  147.  
  148.     # Add a header to be used by the HTTP interface only
  149.     # e.g. u.addheader('Accept', 'sound/basic')
  150.     def addheader(self, *args):
  151.         self.addheaders.append(args)
  152.  
  153.     # External interface
  154.     # Use URLopener().open(file) instead of open(file, 'r')
  155.     def open(self, fullurl):
  156.         fullurl = unwrap(fullurl)
  157.         type, url = splittype(fullurl)
  158.          if not type: type = 'file'
  159.         name = 'open_' + type
  160.         if '-' in name:
  161.             import regsub
  162.             name = regsub.gsub('-', '_', name)
  163.         if not hasattr(self, name):
  164.             return self.open_unknown(fullurl)
  165.         try:
  166.             return getattr(self, name)(url)
  167.         except socket.error, msg:
  168.             raise IOError, ('socket error', msg)
  169.  
  170.     # Overridable interface to open unknown URL type
  171.     def open_unknown(self, fullurl):
  172.         type, url = splittype(fullurl)
  173.         raise IOError, ('url error', 'unknown url type', type)
  174.  
  175.     # External interface
  176.     # retrieve(url) returns (filename, None) for a local object
  177.     # or (tempfilename, headers) for a remote object
  178.     def retrieve(self, url):
  179.         if self.tempcache and self.tempcache.has_key(url):
  180.             return self.tempcache[url]
  181.         url1 = unwrap(url)
  182.         if self.tempcache and self.tempcache.has_key(url1):
  183.             self.tempcache[url] = self.tempcache[url1]
  184.             return self.tempcache[url1]
  185.         type, url1 = splittype(url1)
  186.         if not type or type == 'file':
  187.             try:
  188.                 fp = self.open_local_file(url1)
  189.                 del fp
  190.                 return url2pathname(splithost(url1)[1]), None
  191.             except IOError, msg:
  192.                 pass
  193.         fp = self.open(url)
  194.         headers = fp.info()
  195.         import tempfile
  196.         tfn = tempfile.mktemp()
  197.         result = tfn, headers
  198.         if self.tempcache is not None:
  199.             self.tempcache[url] = result
  200.         tfp = open(tfn, 'w')
  201.         bs = 1024*8
  202.         block = fp.read(bs)
  203.         while block:
  204.             tfp.write(block)
  205.             block = fp.read(bs)
  206.         del fp
  207.         del tfp
  208.         return result
  209.  
  210.     # Each method named open_<type> knows how to open that type of URL
  211.  
  212.     # Use HTTP protocol
  213.     def open_http(self, url):
  214.         import httplib
  215.         host, selector = splithost(url)
  216.         if not host: raise IOError, ('http error', 'no host given')
  217.         i = string.find(host, '@')
  218.         if i >= 0:
  219.             user_passwd, host = host[:i], host[i+1:]
  220.         else:
  221.             user_passwd = None
  222.         if user_passwd:
  223.             import base64
  224.             auth = string.strip(base64.encodestring(user_passwd))
  225.         else:
  226.             auth = None
  227.         h = httplib.HTTP(host)
  228.         h.putrequest('GET', selector)
  229.         if auth: h.putheader('Authorization: Basic %s' % auth)
  230.         for args in self.addheaders: apply(h.putheader, args)
  231.         h.endheaders()
  232.         errcode, errmsg, headers = h.getreply()
  233.         fp = h.getfile()
  234.         if errcode == 200:
  235.             return addinfo(fp, headers)
  236.         else:
  237.             return self.http_error(url,
  238.                            fp, errcode, errmsg, headers)
  239.  
  240.     # Handle http errors.
  241.     # Derived class can override this, or provide specific handlers
  242.     # named http_error_DDD where DDD is the 3-digit error code
  243.     def http_error(self, url, fp, errcode, errmsg, headers):
  244.         # First check if there's a specific handler for this error
  245.         name = 'http_error_%d' % errcode
  246.         if hasattr(self, name):
  247.             method = getattr(self, name)
  248.             result = method(url, fp, errcode, errmsg, headers)
  249.             if result: return result
  250.         return self.http_error_default(
  251.             url, fp, errcode, errmsg, headers)
  252.  
  253.     # Default http error handler: close the connection and raises IOError
  254.     def http_error_default(self, url, fp, errcode, errmsg, headers):
  255.         void = fp.read()
  256.         fp.close()
  257.         raise IOError, ('http error', errcode, errmsg, headers)
  258.  
  259.     # Use Gopher protocol
  260.     def open_gopher(self, url):
  261.         import gopherlib
  262.         host, selector = splithost(url)
  263.         if not host: raise IOError, ('gopher error', 'no host given')
  264.         type, selector = splitgophertype(selector)
  265.         selector, query = splitquery(selector)
  266.         selector = unquote(selector)
  267.         if query:
  268.             query = unquote(query)
  269.             fp = gopherlib.send_query(selector, query, host)
  270.         else:
  271.             fp = gopherlib.send_selector(selector, host)
  272.         return addinfo(fp, noheaders())
  273.  
  274.     # Use local file or FTP depending on form of URL
  275.     def open_file(self, url):
  276.         if url[:2] == '//':
  277.             return self.open_ftp(url)
  278.         else:
  279.             return self.open_local_file(url)
  280.  
  281.     # Use local file
  282.     def open_local_file(self, url):
  283.         host, file = splithost(url)
  284.         if not host: return addinfo(open(url2pathname(file), 'r'), noheaders())
  285.         host, port = splitport(host)
  286.         if not port and socket.gethostbyname(host) in (
  287.               localhost(), thishost()):
  288.             file = unquote(file)
  289.             return addinfo(open(url2pathname(file), 'r'), noheaders())
  290.         raise IOError, ('local file error', 'not on local host')
  291.  
  292.     # Use FTP protocol
  293.     def open_ftp(self, url):
  294.         host, path = splithost(url)
  295.         if not host: raise IOError, ('ftp error', 'no host given')
  296.         host, port = splitport(host)
  297.         user, host = splituser(host)
  298.         if user: user, passwd = splitpasswd(user)
  299.         else: passwd = None
  300.         host = socket.gethostbyname(host)
  301.         if not port:
  302.             import ftplib
  303.             port = ftplib.FTP_PORT
  304.         path, attrs = splitattr(path)
  305.         dirs = string.splitfields(path, '/')
  306.         dirs, file = dirs[:-1], dirs[-1]
  307.         if dirs and not dirs[0]: dirs = dirs[1:]
  308.         key = (user, host, port, string.joinfields(dirs, '/'))
  309.         try:
  310.             if not self.ftpcache.has_key(key):
  311.                 self.ftpcache[key] = \
  312.                            ftpwrapper(user, passwd,
  313.                                   host, port, dirs)
  314.             if not file: type = 'D'
  315.             else: type = 'I'
  316.             for attr in attrs:
  317.                 attr, value = splitvalue(attr)
  318.                 if string.lower(attr) == 'type' and \
  319.                    value in ('a', 'A', 'i', 'I', 'd', 'D'):
  320.                     type = string.upper(value)
  321.             return addinfo(self.ftpcache[key].retrfile(file, type),
  322.                   noheaders())
  323.         except ftperrors(), msg:
  324.             raise IOError, ('ftp error', msg)
  325.  
  326.  
  327. # Derived class with handlers for errors we can handle (perhaps)
  328. class FancyURLopener(URLopener):
  329.  
  330.     def __init__(self, *args):
  331.         apply(URLopener.__init__, (self,) + args)
  332.         self.auth_cache = {}
  333.  
  334.     # Default error handling -- don't raise an exception
  335.     def http_error_default(self, url, fp, errcode, errmsg, headers):
  336.         return addinfo(fp, headers)
  337.  
  338.     # Error 302 -- relocated
  339.     def http_error_302(self, url, fp, errcode, errmsg, headers):
  340.         # XXX The server can force infinite recursion here!
  341.         if headers.has_key('location'):
  342.             newurl = headers['location']
  343.         elif headers.has_key('uri'):
  344.             newurl = headers['uri']
  345.         else:
  346.             return
  347.         void = fp.read()
  348.         fp.close()
  349.         return self.open(newurl)
  350.  
  351.     # Error 401 -- authentication required
  352.     # See this URL for a description of the basic authentication scheme:
  353.     # http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt
  354.     def http_error_401(self, url, fp, errcode, errmsg, headers):
  355.         if headers.has_key('www-authenticate'):
  356.             stuff = headers['www-authenticate']
  357.             p = regex.compile(
  358.                 '[ \t]*\([^ \t]+\)[ \t]+realm="\([^"]*\)"')
  359.             if p.match(stuff) >= 0:
  360.                 scheme, realm = p.group(1, 2)
  361.                 if string.lower(scheme) == 'basic':
  362.                     return self.retry_http_basic_auth(
  363.                         url, realm)
  364.  
  365.     def retry_http_basic_auth(self, url, realm):
  366.         host, selector = splithost(url)
  367.         i = string.find(host, '@') + 1
  368.         host = host[i:]
  369.         user, passwd = self.get_user_passwd(host, realm, i)
  370.         if not (user or passwd): return None
  371.         host = user + ':' + passwd + '@' + host
  372.         newurl = '//' + host + selector
  373.         return self.open_http(newurl)
  374.  
  375.     def get_user_passwd(self, host, realm, clear_cache = 0):
  376.         key = realm + '@' + string.lower(host)
  377.         if self.auth_cache.has_key(key):
  378.             if clear_cache:
  379.                 del self.auth_cache[key]
  380.             else:
  381.                 return self.auth_cache[key]
  382.         user, passwd = self.prompt_user_passwd(host, realm)
  383.         if user or passwd: self.auth_cache[key] = (user, passwd)
  384.         return user, passwd
  385.  
  386.     def prompt_user_passwd(self, host, realm):
  387.         # Override this in a GUI environment!
  388.         try:
  389.             user = raw_input("Enter username for %s at %s: " %
  390.                      (realm, host))
  391.             self.echo_off()
  392.             try:
  393.                 passwd = raw_input(
  394.                   "Enter password for %s in %s at %s: " %
  395.                   (user, realm, host))
  396.             finally:
  397.                 self.echo_on()
  398.             return user, passwd
  399.         except KeyboardInterrupt:
  400.             return None, None
  401.  
  402.     def echo_off(self):
  403.         import os
  404.         os.system("stty -echo")
  405.  
  406.     def echo_on(self):
  407.         import os
  408.         print
  409.         os.system("stty echo")
  410.  
  411.  
  412. # Utility functions
  413.  
  414. # Return the IP address of the magic hostname 'localhost'
  415. _localhost = None
  416. def localhost():
  417.     global _localhost
  418.     if not _localhost:
  419.         _localhost = socket.gethostbyname('localhost')
  420.     return _localhost
  421.  
  422. # Return the IP address of the current host
  423. _thishost = None
  424. def thishost():
  425.     global _thishost
  426.     if not _thishost:
  427.         _thishost = socket.gethostbyname(socket.gethostname())
  428.     return _thishost
  429.  
  430. # Return the set of errors raised by the FTP class
  431. _ftperrors = None
  432. def ftperrors():
  433.     global _ftperrors
  434.     if not _ftperrors:
  435.         import ftplib
  436.         _ftperrors = (ftplib.error_reply,
  437.                   ftplib.error_temp,
  438.                   ftplib.error_perm,
  439.                   ftplib.error_proto)
  440.     return _ftperrors
  441.  
  442. # Return an empty mimetools.Message object
  443. _noheaders = None
  444. def noheaders():
  445.     global _noheaders
  446.     if not _noheaders:
  447.         import mimetools
  448.         import StringIO
  449.         _noheaders = mimetools.Message(StringIO.StringIO(), 0)
  450.         _noheaders.fp.close()    # Recycle file descriptor
  451.     return _noheaders
  452.  
  453.  
  454. # Utility classes
  455.  
  456. # Class used by open_ftp() for cache of open FTP connections
  457. class ftpwrapper:
  458.     def __init__(self, user, passwd, host, port, dirs):
  459.         self.user = unquote(user or '')
  460.         self.passwd = unquote(passwd or '')
  461.         self.host = host
  462.         self.port = port
  463.         self.dirs = []
  464.         for dir in dirs:
  465.             self.dirs.append(unquote(dir))
  466.         self.init()
  467.     def init(self):
  468.         import ftplib
  469.         self.ftp = ftplib.FTP()
  470.         self.ftp.connect(self.host, self.port)
  471.         self.ftp.login(self.user, self.passwd)
  472.         for dir in self.dirs:
  473.             self.ftp.cwd(dir)
  474.     def retrfile(self, file, type):
  475.         import ftplib
  476.         if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1
  477.         else: cmd = 'TYPE ' + type; isdir = 0
  478.         try:
  479.             self.ftp.voidcmd(cmd)
  480.         except ftplib.all_errors:
  481.             self.init()
  482.             self.ftp.voidcmd(cmd)
  483.         conn = None
  484.         if file and not isdir:
  485.             try:
  486.                 cmd = 'RETR ' + file
  487.                 conn = self.ftp.transfercmd(cmd)
  488.             except ftplib.error_perm, reason:
  489.                 if reason[:3] != '550':
  490.                     raise IOError, ('ftp error', reason)
  491.         if not conn:
  492.             # Try a directory listing
  493.             if file: cmd = 'LIST ' + file
  494.             else: cmd = 'LIST'
  495.             conn = self.ftp.transfercmd(cmd)
  496.         return addclosehook(conn.makefile('rb'), self.ftp.voidresp)
  497.  
  498. # Base class for addinfo and addclosehook
  499. class addbase:
  500.     def __init__(self, fp):
  501.         self.fp = fp
  502.         self.read = self.fp.read
  503.         self.readline = self.fp.readline
  504.         self.readlines = self.fp.readlines
  505.         self.fileno = self.fp.fileno
  506.     def __repr__(self):
  507.         return '<%s at %s whose fp = %s>' % (
  508.               self.__class__.__name__, `id(self)`, `self.fp`)
  509.     def close(self):
  510.         self.read = None
  511.         self.readline = None
  512.         self.readlines = None
  513.         self.fileno = None
  514.         if self.fp: self.fp.close()
  515.         self.fp = None
  516.  
  517. # Class to add a close hook to an open file
  518. class addclosehook(addbase):
  519.     def __init__(self, fp, closehook, *hookargs):
  520.         addbase.__init__(self, fp)
  521.         self.closehook = closehook
  522.         self.hookargs = hookargs
  523.     def close(self):
  524.         if self.closehook:
  525.             apply(self.closehook, self.hookargs)
  526.             self.closehook = None
  527.             self.hookargs = None
  528.         addbase.close(self)
  529.  
  530. # class to add an info() method to an open file
  531. class addinfo(addbase):
  532.     def __init__(self, fp, headers):
  533.         addbase.__init__(self, fp)
  534.         self.headers = headers
  535.     def info(self):
  536.         return self.headers
  537.  
  538.  
  539. # Utility to combine a URL with a base URL to form a new URL
  540.  
  541. def basejoin(base, url):
  542.     type, path = splittype(url)
  543.     if type:
  544.         # if url is complete (i.e., it contains a type), return it
  545.         return url
  546.     host, path = splithost(path)
  547.     type, basepath = splittype(base) # inherit type from base
  548.     if host:
  549.         # if url contains host, just inherit type
  550.         if type: return type + '://' + host + path
  551.         else:
  552.             # no type inherited, so url must have started with //
  553.             # just return it
  554.             return url
  555.     host, basepath = splithost(basepath) # inherit host
  556.     basepath, basetag = splittag(basepath) # remove extraneuous cruft
  557.     basepath, basequery = splitquery(basepath) # idem
  558.     if path[:1] != '/':
  559.         # non-absolute path name
  560.         if path[:1] in ('#', '?'):
  561.             # path is just a tag or query, attach to basepath
  562.             i = len(basepath)
  563.         else:
  564.             # else replace last component
  565.             i = string.rfind(basepath, '/')
  566.         if i < 0:
  567.             # basepath not absolute
  568.             if host:
  569.                 # host present, make absolute
  570.                 basepath = '/'
  571.             else:
  572.                 # else keep non-absolute
  573.                 basepath = ''
  574.         else:
  575.             # remove last file component
  576.             basepath = basepath[:i+1]
  577.         path = basepath + path
  578.     if type and host: return type + '://' + host + path
  579.     elif type: return type + ':' + path
  580.     elif host: return '//' + host + path # don't know what this means
  581.     else: return path
  582.  
  583.  
  584. # Utilities to parse URLs (most of these return None for missing parts):
  585. # unwrap('<URL:type://host/path>') --> 'type://host/path'
  586. # splittype('type:opaquestring') --> 'type', 'opaquestring'
  587. # splithost('//host[:port]/path') --> 'host[:port]', '/path'
  588. # splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'
  589. # splitpasswd('user:passwd') -> 'user', 'passwd'
  590. # splitport('host:port') --> 'host', 'port'
  591. # splitquery('/path?query') --> '/path', 'query'
  592. # splittag('/path#tag') --> '/path', 'tag'
  593. # splitattr('/path;attr1=value1;attr2=value2;...') ->
  594. #   '/path', ['attr1=value1', 'attr2=value2', ...]
  595. # splitvalue('attr=value') --> 'attr', 'value'
  596. # splitgophertype('/Xselector') --> 'X', 'selector'
  597. # unquote('abc%20def') -> 'abc def'
  598. # quote('abc def') -> 'abc%20def')
  599.  
  600. def unwrap(url):
  601.     url = string.strip(url)
  602.     if url[:1] == '<' and url[-1:] == '>':
  603.         url = string.strip(url[1:-1])
  604.     if url[:4] == 'URL:': url = string.strip(url[4:])
  605.     return url
  606.  
  607. _typeprog = regex.compile('^\([^/:]+\):\(.*\)$')
  608. def splittype(url):
  609.     if _typeprog.match(url) >= 0: return _typeprog.group(1, 2)
  610.     return None, url
  611.  
  612. _hostprog = regex.compile('^//\([^/]+\)\(.*\)$')
  613. def splithost(url):
  614.     if _hostprog.match(url) >= 0: return _hostprog.group(1, 2)
  615.     return None, url
  616.  
  617. _userprog = regex.compile('^\([^@]*\)@\(.*\)$')
  618. def splituser(host):
  619.     if _userprog.match(host) >= 0: return _userprog.group(1, 2)
  620.     return None, host
  621.  
  622. _passwdprog = regex.compile('^\([^:]*\):\(.*\)$')
  623. def splitpasswd(user):
  624.     if _passwdprog.match(user) >= 0: return _passwdprog.group(1, 2)
  625.     return user, None
  626.  
  627. _portprog = regex.compile('^\(.*\):\([0-9]+\)$')
  628. def splitport(host):
  629.     if _portprog.match(host) >= 0: return _portprog.group(1, 2)
  630.     return host, None
  631.  
  632. _queryprog = regex.compile('^\(.*\)\?\([^?]*\)$')
  633. def splitquery(url):
  634.     if _queryprog.match(url) >= 0: return _queryprog.group(1, 2)
  635.     return url, None
  636.  
  637. _tagprog = regex.compile('^\(.*\)#\([^#]*\)$')
  638. def splittag(url):
  639.     if _tagprog.match(url) >= 0: return _tagprog.group(1, 2)
  640.     return url, None
  641.  
  642. def splitattr(url):
  643.     words = string.splitfields(url, ';')
  644.     return words[0], words[1:]
  645.  
  646. _valueprog = regex.compile('^\([^=]*\)=\(.*\)$')
  647. def splitvalue(attr):
  648.     if _valueprog.match(attr) >= 0: return _valueprog.group(1, 2)
  649.     return attr, None
  650.  
  651. def splitgophertype(selector):
  652.     if selector[:1] == '/' and selector[1:2]:
  653.         return selector[1], selector[2:]
  654.     return None, selector
  655.  
  656. _quoteprog = regex.compile('%[0-9a-fA-F][0-9a-fA-F]')
  657. def unquote(s):
  658.     i = 0
  659.     n = len(s)
  660.     res = ''
  661.     while 0 <= i < n:
  662.         j = _quoteprog.search(s, i)
  663.         if j < 0:
  664.             res = res + s[i:]
  665.             break
  666.         res = res + (s[i:j] + chr(string.atoi(s[j+1:j+3], 16)))
  667.         i = j+3
  668.     return res
  669.  
  670. always_safe = string.letters + string.digits + '_,.-'
  671. def quote(s, safe = '/'):
  672.     safe = always_safe + safe
  673.     res = ''
  674.     for c in s:
  675.         if c in safe:
  676.             res = res + c
  677.         else:
  678.             res = res + '%%%02x' % ord(c)
  679.     return res
  680.  
  681. # Test and time quote() and unquote()
  682. def test1():
  683.     import time
  684.     s = ''
  685.     for i in range(256): s = s + chr(i)
  686.     s = s*4
  687.     t0 = time.time()
  688.     qs = quote(s)
  689.     uqs = unquote(qs)
  690.     t1 = time.time()
  691.     if uqs != s:
  692.         print 'Wrong!'
  693.     print `s`
  694.     print `qs`
  695.     print `uqs`
  696.     print round(t1 - t0, 3), 'sec'
  697.  
  698.  
  699. # Test program
  700. def test():
  701.     import sys
  702.     import regsub
  703.     args = sys.argv[1:]
  704.     if not args:
  705.         args = [
  706.             '/etc/passwd',
  707.             'file:/etc/passwd',
  708.             'file://localhost/etc/passwd',
  709.             'ftp://ftp.cwi.nl/etc/passwd',
  710.             'gopher://gopher.cwi.nl/11/',
  711.             'http://www.cwi.nl/index.html',
  712.             ]
  713.     try:
  714.         for url in args:
  715.             print '-'*10, url, '-'*10
  716.             fn, h = urlretrieve(url)
  717.             print fn, h
  718.             if h:
  719.                 print '======'
  720.                 for k in h.keys(): print k + ':', h[k]
  721.                 print '======'
  722.             fp = open(fn, 'r')
  723.             data = fp.read()
  724.             del fp
  725.             print regsub.gsub('\r', '', data)
  726.             fn, h = None, None
  727.         print '-'*40
  728.     finally:
  729.         urlcleanup()
  730.  
  731. # Run test program when run as a script
  732. if __name__ == '__main__':
  733. ##    test1()
  734.     test()
  735.